﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace Djs.Tools.XmlPersistor
{
    // all internal
	#region XmDocument: obálka nad objektem System.Xml.Linq.XDocument
	/// <summary>
	/// XmDocument: obálka nad objektem System.Xml.Linq.XDocument
	/// </summary>
	internal class XmDocument
	{
		#region Konstrukce
		private XmDocument(System.Xml.Linq.XDocument xDoc)
		{
			this._XDocument = xDoc;
			this._CurrentElement = null;
		}
		/// <summary>
		/// Vrátí XML dokument z daného souboru
		/// </summary>
		/// <param name="fileName"></param>
		/// <returns></returns>
		internal static XmDocument CreateFromFile(string fileName)
		{
			if (!String.IsNullOrEmpty(fileName) && System.IO.File.Exists(fileName))
			{
				ResolveUtfMarker(fileName);     // Starší verze XmlPersist ukládaly XML soubor v kódování UTF-8, ale psaly do něj UTF-16. To se musí vyřešit nyní.
				XmDocument doc = new XmDocument(System.Xml.Linq.XDocument.Load(fileName));
				return doc;
			}
			return null;
		}
		/// <summary>
		/// Starší verze XmlPersist ukládaly XML soubor v kódování UTF-8, ale psaly do něj UTF-16. To se musí vyřešit nyní.
		/// </summary>
		/// <param name="fileName"></param>
		internal static void ResolveUtfMarker(string fileName)
		{
			if (String.IsNullOrEmpty(fileName) || !System.IO.File.Exists(fileName))
				return;
			string content = System.IO.File.ReadAllText(fileName);
			if (content.Contains(_MARKER_BAD))
			{
				content = content.Replace(_MARKER_BAD, _MARKER_CORRECT);
				System.IO.File.WriteAllText(fileName, content, Encoding.UTF8);
			}
		}
		private const string _MARKER_BAD = "<?xml version=\"1.0\" encoding=\"utf-16\"?>";
		private const string _MARKER_CORRECT = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";

		/// <summary>
		/// Vrátí XML dokument z daného textu
		/// </summary>
		/// <param name="xmlContent"></param>
		/// <returns></returns>
		internal static XmDocument CreateFromString(string xmlContent)
		{
			if (!String.IsNullOrEmpty(xmlContent))
			{
				XmDocument doc = new XmDocument(System.Xml.Linq.XDocument.Parse(xmlContent));
				return doc;
			}
			return null;
		}
		public override string ToString()
		{
			if (this._XDocument == null) return "null";
			if (this._XDocument.Root == null) return "Empty";
			return "XmlDocument: " + this._XDocument.Root.Name.LocalName;
		}
		private System.Xml.Linq.XDocument _XDocument;
		private XmElement _CurrentElement;
		#endregion
		#region Hledání
		/// <summary>
		/// Vyhledá a vrátí přesně zadaný element.
		/// Zadání se provádí uvedením všech elementů počínaje Root elementem, až k elementu který chci vrátit.
		/// </summary>
		/// <param name="elementNames"></param>
		/// <returns></returns>
		internal XmElement FindElement(params string[] elementNames)
		{
			XElement findIn = this._XDocument.Root;
			XmElement result = null;
			if (elementNames != null && elementNames.Length > 0)
			{
				int len = elementNames.Length;
				if (findIn.Name.LocalName == elementNames[0])
				{
					if (len == 1)
						result = XmElement.Create(findIn);
					else
					{
						for (int i = 1; i < len; i++)
						{
							string elementName = elementNames[i];
							// XElement find = findIn.Element(elementName);
							XElement find = findIn.Elements().FirstOrDefault(xe => xe.Name.LocalName == elementName);

							if (find == null) break;
							if (i < (len - 1))
							{
								findIn = find;
							}
							else
							{
								result = XmElement.Create(find);
								break;
							}
						}
					}
				}
			}
			this._CurrentElement = result;
			return result;
		}
		#endregion
	}
	#endregion
	#region XmElement: obálka nad objektem System.Xml.Linq.XElement
	/// <summary>
	/// XmElement: obálka nad objektem System.Xml.Linq.XElement
	/// </summary>
	internal class XmElement
	{
		#region Konstrukce
		private XmElement(System.Xml.Linq.XElement xEle)
		{
			this._XElement = xEle;
			this._XmAttributes = null;
		}
		/// <summary>
		/// Vrátí obálku XML elementu pro daný objekt
		/// </summary>
		/// <param name="findIn"></param>
		/// <returns></returns>
		internal static XmElement Create(XElement xEle)
		{
			XmElement xmEle = null;
			if (xEle != null)
				xmEle = new XmElement(xEle);
			return xmEle;
		}
		public override string ToString()
		{
			if (this._XElement == null) return "null";
			if (this._XElement.Name == null) return "Empty";
			return "XmlElement: " + this._XElement.Name.LocalName;
		}
		private System.Xml.Linq.XElement _XElement;
		private XmAttributes _XmAttributes;
		#endregion
		#region Property
        /// <summary>
        /// Jméno elementu, lokální (bez namespace)
        /// </summary>
        internal string Name { get { return this._XElement.Name.LocalName; } }
		/// <summary>
        /// Aktuální XElement = přímý přístup k objektu System.Xml.Linq.XElement, pro který je tento XmElement vytvořen
		/// </summary>
		internal XElement XElement { get { return this._XElement; } }
        /// <summary>
        /// Přístup k vnitřním elementům v tomto elementu.
        /// Vnitřní elementy jsou elementy právě v úrovni this.Childs, nikoli hlouběji zanořené.
        /// Vnitřní elementy jsou opět třídy shodné jako this (tj. XmElement), nejde o elementy System.Xml.Linq.XElement.
        /// </summary>
        internal IEnumerable<XmElement> XmElements { get { return new XmElementList(this); } }
        /// <summary>
		/// Zmapovaná sada atributů
		/// </summary>
		internal XmAttributes XmAttributes
		{
			get
			{
				if (this._XmAttributes == null)
					this._ReadAttributes();
				return this._XmAttributes;
			}
		}
		#endregion
		#region Zmapování atributů
		private void _ReadAttributes()
		{
			this._XmAttributes = new XmAttributes(this);
			if (!this._XElement.HasAttributes) return;
			foreach (XAttribute xAtr in this._XElement.Attributes())
				this._XmAttributes.Add(xAtr);
		}
		#endregion
		#region Enumerace vnitřních elementů (enumerace elementů XmElement, nikoli XElement)
        protected class XmElementList : IEnumerable<XmElement>
        {
            internal XmElementList(XmElement parent)
            {
                this.Parent = parent;
            }
            protected XmElement Parent;
            IEnumerator<XmElement> IEnumerable<XmElement>.GetEnumerator()
            {
                return _GetEnumerator();
            }
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return _GetEnumerator();
            }
            /// <summary>
            /// Tvorba enumerátoru: z dat třídy XElement (systémová) z _XElement.Elements() 
            /// vytvořím položky třídy XmElement (zdejší) do lokálního seznamu, z nějž vrátím jeho enumerátor.
            /// </summary>
            /// <returns></returns>
            private IEnumerator<XmElement> _GetEnumerator()
            {
                if (this.Parent == null) return null;
                List<XmElement> list = new List<XmElement>();
                foreach (XElement xEle in this.Parent._XElement.Elements())
                    list.Add(XmElement.Create(xEle));
                return list.GetEnumerator();
            }
        }
        #endregion
		#region Vyhledání atributu a elementu
        /// <summary>
        /// Najde ve zdejších atributech atribut daného jména (case sensitive) a vrátí jeho hodnotu Value. Pokud je jich víc, vrátí první.
        /// Pokud neexistuje atribut, nebo neobsahuje value, vrací defaultValue.
        /// </summary>
        /// <param name="attributeName"></param>
        /// <returns></returns>
        internal XmAttribute TryFindAttribute(string attributeName)
        {
            return this.XmAttributes.TryFind(attributeName);
        }
        /// <summary>
        /// Najde ve zdejších atributech atribut daného jména (case sensitive) a vrátí jeho hodnotu Value. Pokud je jich víc, vrátí první.
        /// Pokud neexistuje atribut, nebo neobsahuje value, vrací defaultValue.
        /// </summary>
        /// <param name="attributeName"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        internal string FindAttributeValue(string attributeName, string defaultValue)
        {
            XmAttribute attr = this.XmAttributes.TryFind(attributeName);
            if (attr == null) return defaultValue;
            string value = attr.ValueFirstOrDefault;
            if (value == null) return defaultValue;
            return value;
        }
        /// <summary>
        /// Najde ve svých elementech element s daným jménem a vrátí jej.
        /// Pokud nenajde, vrátí null.
        /// </summary>
        /// <param name="elementName"></param>
        /// <returns></returns>
        internal XmElement TryFindElement(string elementName)
        {
            if (String.IsNullOrEmpty(elementName))
                return null;
            return this.XmElements.FirstOrDefault(e => e.Name == elementName);
        }
        #endregion
    }
	#endregion
	#region XmAttributes a XmAttribute: slovník atributů
	/// <summary>
	/// Soupis atributů jednoho elementu
	/// </summary>
	internal class XmAttributes : IEnumerable<XmAttribute>
	{
		#region Konstrukce
		internal XmAttributes(XmElement xmEle)
		{
			this._XmElement = xmEle;
			this._AttributeDict = new Dictionary<string, XmAttribute>();
		}
		private XmElement _XmElement;
		private Dictionary<string, XmAttribute> _AttributeDict;
		/// <summary>
		/// Do sady atributů přidá hodnotu načtenou z dalšího atributu.
		/// Detekuje přitom lokální jména s tečkou, separuje název před tečkou.
		/// </summary>
		/// <param name="xAtr"></param>
		internal void Add(XAttribute xAtr)
		{
			// Rozdělím název atributu v místě tečky:
			string aNam = xAtr.Name.LocalName;          // Jméno, například "Value", nebo "Value.Type", nebo "Array.Range"
			string aSfx = null;                         // Zůstane null pro atributy, které ve jménu nemají tečku
			if (!String.IsNullOrEmpty(aNam) && aNam.Contains('.'))
			{
				int len = aNam.Length;
				int iDot = aNam.IndexOf('.');
				aSfx = (iDot < (len - 1) ? aNam.Substring(iDot + 1) : "");      // Text před tečkou = název logického atributu
				aNam = (iDot > 0 ? aNam.Substring(0, iDot) : "");               // Text za tečkou   = určuje property v XmAttribute, do které se uloží hodnota Value
			}

			// Najdu / vytvořím atribut:
			XmAttribute xmAtt;
			if (!this._AttributeDict.TryGetValue(aNam, out xmAtt))
			{
				xmAtt = new XmAttribute(aNam);
				this._AttributeDict.Add(aNam, xmAtt);
			}

			// Uložím suffix a hodnotu:
			xmAtt.Add(aSfx, xAtr.Value);
		}
        /// <summary>
        /// Najde a vrátí data pro daný název atributu, nebo vrátí null.
        /// </summary>
        /// <param name="attributeName"></param>
        /// <returns></returns>
        internal XmAttribute TryFind(string attributeName)
        {
            XmAttribute result;
            if (this._AttributeDict.TryGetValue(attributeName, out result))
                return result;
            return null;
        }
		#endregion
		#region IEnumerable<XmAttribute> Members
		IEnumerator<XmAttribute> IEnumerable<XmAttribute>.GetEnumerator() { return this._AttributeDict.Values.GetEnumerator(); }
		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this._AttributeDict.Values.GetEnumerator(); }
		#endregion
	}
	/// <summary>
	/// XmAttribute: jeden atribut, logická jednotka (shrnutá z více záznamů shodného jména).
	/// Pokud jsou v elementu uvedeny atributy například Item = "hodnota", Item.Type = "String", Item.Indices = "[0,2,6]", 
	/// budou tyto atributy shrnuty v jednom objektu třídy XmAttribute
	/// </summary>
	internal class XmAttribute
	{
		#region Konstrukce
		internal XmAttribute(string name)
		{
			this.Name = name;
			this.Values = new List<string>();
			this.Suffixes = new Dictionary<string, string>();
		}
		public override string ToString()
		{
			string result = this.Name;
			string del = " = ";
			foreach (string value in this.Values)
			{
				result += del + value;
				del = ", ";
			}
			return result;
		}
		#endregion
		#region Property
		internal string Name { get; private set; }
		internal string Type { get { return GetSuffix("Type"); } }
        internal string Assembly { get { return GetSuffix("Assembly"); } }
		internal string Range { get { return GetSuffix("Range"); } }
		internal string Indices { get { return GetSuffix("Indices"); } }
		internal List<string> Values { get; private set; }
        internal string ValueFirstOrDefault { get { return (this.Values.Count > 0 ? this.Values[0] : null); } }
		internal Dictionary<string, string> Suffixes { get; private set; }
		#endregion
		#region Metody Add, ContainSuffix, GetSuffix
		/// <summary>
		/// Uloží do sebe hodnotu podle uvedeného suffixu.
		/// Příklad pro atribut: Value.Type = "Int32"
		/// Název atributu (this.Name) = "Value"
		/// Zde doddaný suffix = "Type", určuje že hodnota value se bude ukládat do property this.Type.
		/// Zde dodaná hodnota value nese vlastní data.
		/// </summary>
		/// <param name="suffix"></param>
		/// <param name="value"></param>
		internal void Add(string suffix, string value)
		{
			if (suffix == null)
				this.Values.Add(value);
			else
			{
				if (!this.Suffixes.ContainsKey(suffix))
					this.Suffixes.Add(suffix, value);
				else
					this.Suffixes[suffix] = value;
			}
		}
		/// <summary>
		/// Vrací příznak, zda tento atribut obsahuje daný suffix.
		/// </summary>
		/// <param name="suffix"></param>
		/// <returns></returns>
		internal bool ContainSuffix(string suffix)
		{
			if (suffix == null) return false;
			return this.Suffixes.ContainsKey(suffix);
		}
		/// <summary>
		/// Vrací hodnotu uloženou za daným suffixem.
		/// Pokud nebyl nalezen atribut se jménem this.Name + "." + suffix, vrátí null.
		/// </summary>
		/// <param name="suffix"></param>
		/// <returns></returns>
		internal string GetSuffix(string suffix)
		{
			if (suffix == null) return null;
			if (!this.Suffixes.ContainsKey(suffix)) return null;
			return this.Suffixes[suffix];
		}
		#endregion
	}
	#endregion
}
